/**------------------------------------*------------------------------------**
    RELEASED TO THE PUBLIC DOMAIN BY CODEWORKS DEVELOPMENT, DECEMBER 1993.
    You may freely copy, distribute and reuse the code in this example. Codeworks disclaims any warranty of any kind, expressed or implied, as to its fitness for any particular use.
    Author:  Andrew Vyrros, av@codeworks.com
 **------------------------------------*------------------------------------**/
/**------------------------------------*------------------------------------**
    File:    Manager.m
    Project: DynaDoodle
    Info:    Implementation of the Manager class. Manager acts as the central object for the DynaDoodle application, and is NXApp's delegate.  It coordinates the loading of doodle modules and the selection of the current doodle. It also forwards some graphical control methods to the current doodle.
    In its implementation, Manager is tied explicitly to DoodleModule and Doodle. It would have been possible to create a version with far less dependence on these classes. Such a version would have made it easier to use Manager in other projects, but I felt that this would have obscured the concepts and constructs of dynamic loading. I hope that once you understand the basic structure, you can easily adapt it to your own uses.
 **------------------------------------*------------------------------------**/

#import "Manager.h"
#import "DoodleBundle.h"
#import "Doodle.h"
#import <appkit/appkit.h>
#import <sys/dir.h>


/* Subdirectory in Library directory to search for Doodle modules. */
#define LIBRARYSUBPATH           "DynaDoodle"
/*
 * Extension for Doodle modules. The default extension for Project Builder
 * modules is .bundle. If you want to use the more appropriate .doodle, you
 * can switch it here. But then you will need to change the names of the
 * module packages, too. In NEXTSTEP 3.2, you can do this directly from PB.
 * In 3.1 or 3.0, there is no UI to do this in PB, and the Makefile rules
 * for BUNDLE_EXTENSION don't work right either, so you will either need to
 * come up with your own Makefile hacks, or change the extensions by hand.
 * For maximum compatibility, I have left it set to the default .bundle.
*/
#define MODULEEXTENSION          "bundle"
//#define MODULEEXTENSION          "doodle"


@implementation Manager


/**------------------------------------*------------------------------------**
    Method:  appDidInit:
    Info:    Applicaton delegate method, informs Manager that NXApp has been initialized. Manager uses this opportunity to create the Bundles, fill a PopUpList with the available Doodles, and set the current Doodle to a default setting.
    Return:  (id)self
 **------------------------------------*------------------------------------**/
- appDidInit:sender
{
  /*
   * Create List of Bundles. This will only create the Bundle objects, it
   * will not load any modules or create any objects within the modules. By
   * changing the argument to YES, it will load the modules, too. This way
   * you can see the performance tradeoff of loading modules as needed vs.
   * loading all modules at launch time. 
   */
  bundleList = [[List allocFromZone:[self zone]] init];
  [self createBundlesAndLoadModules:NO];
//  [self createBundlesAndLoadModules:YES];
  
  /*
   * Fill currentDoodlePopUpList with available modules, then set current
   * doodle to default. 
   */
  [self fillCurrentDoodlePopUpList];
  [self setCurrentDoodleToDefault];
  
  /*
   * Because Interface Builder does not allow you to set the target of a
   * PopUpList, do it programmatically now. 
   */
  [[[currentDoodleButton target] setTarget:self]
   setAction:@selector(takeCurrentDoodleFrom:)];
  
  /* Since the main Window was deferred, present it now. */
  [[doodleBox window] makeKeyAndOrderFront:self];
  
  return self;
}


/**------------------------------------*------------------------------------**
    Method:  free
    Info:    Frees all resources allocated by Manager.
    Return:  (id)nil
 **------------------------------------*------------------------------------**/
- free
{
  /*
   * Just to be safe, remove the currentDoodle from the View hierarchy first,
   * then free everything. 
   */
  [currentDoodle removeFromSuperview];
  bundleList = [[bundleList freeObjects] free];
  return [super free];
}


/**------------------------------------*------------------------------------**
    Method:  createBundlesAndLoadModules:
    Info:    Creates full set of Bundles and adds to bundleList. Searches for modules with extension MODULEEXTENSION. Looks in LIBRARYSUBPATH within Library directories and then app package. If doLoad = YES, will load modules too. Otherwise, just creates Bundles, defers loading of modules until each is needed.
    Return:  (id)self
 **------------------------------------*------------------------------------**/
- createBundlesAndLoadModules:(BOOL)doLoad
{
  char                libraryDir[MAXPATHLEN + 1];
  
  /* Just to be safe, clear out bundleList. */
  [bundleList freeObjects];
  
  /* Create bundles for ~/Library/LIBRARYSUBPATH. */
  strcpy(libraryDir, NXHomeDirectory());
  strcat(libraryDir, "/Library/");
  strcat(libraryDir, LIBRARYSUBPATH);
  [self createBundlesForDirectory:libraryDir loadModules:doLoad];
  
  /* Create bundles for /LocalLibrary/LIBRARYSUBPATH. */
  strcpy(libraryDir, "/LocalLibrary/");
  strcat(libraryDir, LIBRARYSUBPATH);
  [self createBundlesForDirectory:libraryDir loadModules:doLoad];

  /* Create bundles for /NextLibrary/LIBRARYSUBPATH. */
  strcpy(libraryDir, "/NextLibrary/");
  strcat(libraryDir, LIBRARYSUBPATH);
  [self createBundlesForDirectory:libraryDir loadModules:doLoad];

  /* Finally, create bundles for app package. */
  strcpy(libraryDir, [[NXBundle mainBundle] directory]);
  [self createBundlesForDirectory:libraryDir loadModules:doLoad];
    
  return self;
}
  
  
/**------------------------------------*------------------------------------**
    Function:fileNameHasExtension
    Info:    Helper function, determines whether a file name string has the given extension. Used by createBundlesForDirectory:loadModules:.
    Return:  (BOOL)y/n
 **------------------------------------*------------------------------------**/
static BOOL fileNameHasExtension(const char *f, const char *e)
{
  const char         *dot;

  if (f && e && (dot = strrchr(f, '.')) && !strcmp(dot + 1, e))
    return YES;
  else
    return NO;
}


/**------------------------------------*------------------------------------**
    Method:  createBundlesForDirectory:loadModules:
    Info:    Creates set of Bundles for modules in given directory. If doLoad = YES, will load modules too. Otherwise, just creates Bundles, defers loading of modules until each is needed.
    Note that Bundles are added to the List in the order that their module packages are found by readdir(), which can be unpredictable. Depending on your needs, you may want to adjust this order now. See fillCurrentDoodlePopUpList below.
    Return:  (id)self
 **------------------------------------*------------------------------------**/
- createBundlesForDirectory:(const char *)dirPath loadModules:(BOOL)doLoad
{
  char                modulePath[MAXPATHLEN + 1];
  char               *modulePathInsert;
  DIR                *dir;
  struct direct      *dirEntry;
  DoodleBundle       *newDoodleBundle;

  /* Copy dirPath to modulePath, append '/', get set to append module name. */
  strcpy(modulePath, dirPath);
  modulePathInsert = modulePath + strlen(modulePath);
  *modulePathInsert = '/';
  modulePathInsert++;
  
  /* Get a DIR for dirPath. */
  dir = opendir(dirPath);
  if (!dir)
    return self;

  /* Loop through entries in dir. */
  while (dirEntry = readdir(dir))
  {
    /* If entry name starts with '.' or does not have right extension, skip. */
    if ((dirEntry->d_name[0] == '.')
        || !fileNameHasExtension(dirEntry->d_name, MODULEEXTENSION))
      continue;

    /* Copy module name to correct point in modulePath. */
    strcpy(modulePathInsert, dirEntry->d_name);
    
    /* Create new DoodleBundle, add to bundleList. */
    newDoodleBundle = [[DoodleBundle allocFromZone:[self zone]]
                       initForDirectory:modulePath];
    [bundleList addObject:newDoodleBundle];

    /* If asked to load now, force new DoodleBundle to load module. */
    if (doLoad)
      [newDoodleBundle doodle];
  }
  
  /* Clean up after opendir(). */
  closedir(dir);

  return self;
}


/**------------------------------------*------------------------------------**
    Method:  fillCurrentDoodlePopUpList
    Info:    Fills a PopUpList with the names of the available Doodles. Also sets tags of entries for easy identification.
    In a real program, you might want to alphasort the list of module names before putting them into the PopUpList. Even better is to give the user some way to set the order that the modules are listed. This makes the most sense when the selection control is something like a Matrix of ButtonCells. Another bonus option is to let the user add and subtract modules that the program presents. See createBundlesForDirectory:loadModules: above.
    Return:  (id)self
 **------------------------------------*------------------------------------**/
- fillCurrentDoodlePopUpList
{
  PopUpList          *currentDoodlePopUpList;
  int                 i,
                      numBundles;
  const char         *doodleName;

  /* Get the PopUpList from its cover Button. */
  currentDoodlePopUpList = [currentDoodleButton target];

  /* Empty out the PopUpList. */
  i = [currentDoodlePopUpList count];
  while (i--)
    [[currentDoodlePopUpList removeItemAt:i] free];

  /* Step through the Bundles, adding each doodleName to the PopUpList. */
  numBundles = [bundleList count];
  for (i = 0; i < numBundles; i++)
  {
    /*
     * Note that we ask the DoodleBundle for the doodleName, rather than the
     * Doodle itself. If we used Doodle to get the doodleName, we would have
     * to instantiate the Doodle now, which would require loading the module
     * now. This would preclude the lazy loading of modules. So DoodleBundle
     * provides the doodleName instead. 
     */
    doodleName = [[bundleList objectAt:i] doodleName];
    [[currentDoodlePopUpList addItem:doodleName] setTag:i];
  }

  return self;
}


/**------------------------------------*------------------------------------**
    Method:  setCurrentDoodleToDefault
    Info:    Sets the current Doodle to a default setting when the program launches. Also synchronizes the PopUpList with this choice.
    Taking the easy way out, this just gets the Doodle at index 0 in bundleList. A real program would allow the user to set the preferred module to appear at launch time.
    Return:  (id)self
 **------------------------------------*------------------------------------**/
- setCurrentDoodleToDefault
{
  int                 defaultIndex;
  Doodle             *defaultDoodle;
  PopUpList          *currentDoodlePopUpList;

  /* Get Doodle from DoodleBundle at index 0. */
  defaultIndex = 0;
  defaultDoodle = [[bundleList objectAt:defaultIndex] doodle];
  [self setCurrentDoodle:defaultDoodle];

  /* Synchronize PopUpList. */
  currentDoodlePopUpList = [currentDoodleButton target];
  [[currentDoodlePopUpList itemList] selectCellWithTag:defaultIndex];
  [currentDoodleButton setTitle:[currentDoodlePopUpList selectedItem]];

  return self;
}


/**------------------------------------*------------------------------------**
    Method:  setCurrentDoodle:
    Info:    Sets currentDoodle to the supplied Doodle. To do this, it sets the foreground and background colors of the Doodle to the current settings, then puts it into the doodleBox.
    Return:  (id)self
 **------------------------------------*------------------------------------**/
- setCurrentDoodle:(Doodle *)newDoodle
{
  DoodleBundle       *currentDoodleBundle;

  /* If this already is currentDoodle, don't do extra work. */
  if (newDoodle == currentDoodle)
    return self;
  
  /* Update currentDoodle, get ready to muck with Views. */
  currentDoodle = newDoodle;
  [[doodleBox window] disableDisplay];

  /* First make sure that currentDoodle has correct color settings. */
  [currentDoodle setForegroundColor:[foregroundColorWell color]];
  [currentDoodle setBackgroundColor:[backgroundColorWell color]];

  /*
   * Plop currentDoodle into doodleBox. Using setContentView: is not
   * necessarily the smartest way to do it, but it's quick and easy.
   * Unfortunately, this will leak the default contentView that is created
   * when a Box is initialized. This can be avoided, of course, but it's not
   * worth it for this demo. 
   */
  [doodleBox setContentView:currentDoodle];
  
  /* Update descriptionTextField with the new Doodle's description. */
  currentDoodleBundle = [DoodleBundle bundleForClass:[currentDoodle class]];
  [descriptionTextField setStringValue:[currentDoodleBundle doodleDescription]];
  
  /*
   * Plop the new Doodle's customControlView into customControlBox. The same
   * caveat mentioned above still applies. In addition, you should note that
   * setContentView: will resize the supplied View to be the same size as the
   * old contentView, which can screw up your carefully layed out controls.
   * In this project, all the customControlViews exactly fit into
   * customControlBox, so it's not an issue. But a more robust solution would
   * probably center the customControlView in customControlBox. 
   */
  [customControlBox setContentView:[currentDoodle customControlView]];
  
  /* Clean up Views. */
  [[currentDoodle customControlView] update];
  [[doodleBox window] reenableDisplay];
  [[doodleBox window] displayIfNeeded];

  return self;
}
  

/**------------------------------------*------------------------------------**
    Method:  takeCurrentDoodleFrom:
    Info:    Action method, called by PopUpList of Doodles. Sets currentDoodle based on tag of sender, using setCurrentDoodle: to do the work.
    Return:  (id)self
 **------------------------------------*------------------------------------**/
- takeCurrentDoodleFrom:sender
{
  Doodle             *newDoodle;

  newDoodle = [[bundleList objectAt:[sender selectedTag]] doodle];
  [self setCurrentDoodle:newDoodle];

  return self;
}


/**------------------------------------*------------------------------------**
    Method:  takeForegroundColorFrom:, takeBackgroundColorFrom:
    Info:    Action methods, called by foregroundColorWell/backgroundColorWell. Set colors of currentDoodle based on value of sender.
    Return:  (id)self
 **------------------------------------*------------------------------------**/
- takeForegroundColorFrom:sender
{
  [currentDoodle setForegroundColor:[sender color]];

  return self;
}


- takeBackgroundColorFrom:sender
{
  [currentDoodle setBackgroundColor:[sender color]];

  return self;
}


/**------------------------------------*------------------------------------**
    Method:  showInfoPanel:
    Info:    Action method, called by Info... MenuCell. Checks if nib is loaded, then pops up Panel.
    Return:  (id)self, nil if there is a problem.
 **------------------------------------*------------------------------------**/
- showInfoPanel:sender
{
  char                nibPath[MAXPATHLEN + 1];

  /* If infoPanel has not been loaded yet, load it. */
  if (!infoPanel)
  {
    /*
     * Note that we go to the extra trouble to get the Bundle for this class,
     * rather than just looking in the main Bundle with loadNibSection....
     * This is good defensive programming in case this class is moved into
     * its own dynamic module some day--the code will still work perfectly! 
     */
    [[NXBundle bundleForClass:[self class]]
     getPath:nibPath forResource:"Info" ofType:"nib"];
    [NXApp loadNibFile:nibPath owner:self withNames:NO fromZone:[self zone]];
  }

  /* If infoPanel loaded OK, send it out, otherwise, return nil. */
  if (infoPanel)
  {
    [infoPanel makeKeyAndOrderFront:self];
    return self;
  }
  else
    return nil;
}
      

@end
